tooling(pm): derive the line ratchet's lane roster from the lanes/ directory - #16437
Merged
os-steve merged 1 commit intoSep 7, 2026
Merged
Conversation
…rectory The self-test's lane-roster case was an `.every` over eight literal lane names, so the ninth lane file was pinned by nothing: a ceiling row put there for it kept the case green without the case ever naming it, and deleting that row again was caught only by the map-wide cases, which say nothing about a row that is simply gone. The roster is now read from the directory (`readdirSync`, every `*.md`, sorted) and held against the ceiling map, so the tree and the map are two independent sources checked against one another. A new lane file is pinned by construction: no list to extend, and no count in the label to keep in step with the list. A derived roster has one failure mode of its own, and it is the same shape as the defect above -- `[].every(...)` is `true`, so an unreadable directory would report perfect coverage of nothing. The verdict refuses an empty roster outright, and each red path carries a fixture case beside the live one, because a red path that stopped working runs green forever. Moves named: the pinned case label and its expectation-table entry move together (duplicate-label refusal); one case becomes three, so the self-test runs 155 -> 157 cases and SELF_TEST_BATTERY_FLOOR moves 155 -> 157, with the two prose readings of that same number moved with it. No ceiling row changed; no other file. Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox Co-authored-by: Claude <noreply@anthropic.com>
Contributor
Author
Verdict: ACCEPTImplemented-by: os-dev flight T (opus), branch What was checked
Rulings
验收备注 (noted, not filed)
LandingNot governed, ordinary queue landing: the seat flips the draft to ready and arms auto-merge (SQUASH). On merge the seat strips Generated by Claude Code |
os-steve
marked this pull request as ready for review
September 7, 2026 02:18
os-steve
enabled auto-merge
September 7, 2026 02:18
This was referenced Sep 7, 2026
os-steve
deleted the
claude/issue-15965-ratchet-lane-roster-by-construction
branch
September 7, 2026 03:28
This was referenced Sep 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #15965
The self-test's lane-roster case was an
.everyover eight literal lane names, so the ninth lane file (lanes/triage.md, landed by #15958) was pinned by nothing: a ceiling row put there for it kept the case green without the case ever naming it, and deleting that row again was caught only by the map-wide cases, which say nothing about a row that is simply gone.The roster is now read from the
lanes/directory (readdirSync, every*.md, sorted) and held againstCEILINGS, so the tree and the map are two independent sources checked against one another. A new lane file is pinned by construction: no list to extend, and no count in the label to keep in step with the list.A derived roster has one failure mode of its own, and it is the same shape as the defect it repairs —
every()over an empty array istrue, so an unreadable directory would report perfect coverage of nothing. The verdict refuses an empty roster outright, and each red path carries a fixture case beside the live one, because a red path that stopped working runs green forever.One file, self-test only:
scripts/pm/check-skill-line-ratchet.mjs. No ceiling row changed.Every move, named
SELF_TEST_BATTERY_FLOOR(the roster-size pin)The pinned case label and its expectation-table entry moved together in the same commit — that table is half of the duplicate-label refusal. One case became three: the live derived-roster case, plus one fixture case per red path. Nothing else in the battery-floor machinery, the H-rows or the pins was touched.
Reverse verification
Run against the committed implementation, on scratch copies placed at
scripts/pm/depth soREPO_ROOTstill resolves. Each mutation was proved on disk by occurrence count before and after, not by an editor's exit code; the copies were removed afterwards,git diff HEADis empty, and the worktree blobb7db6099e4is identical to the HEAD blob.(a) the defect. Pre-edit file, with
lanes/triage.md's ceiling row and its table-row pin both removed (occurrences(1,1)on disk before,(0,0)after) — the realistic shape of deleting a lane's coverage:The ninth lane file leaves the map entirely, the self-test exits 0, and the roster case whose job this is stays green.
Removing only the ceiling row and leaving the pin row behind reds a different case (
and the pin map names no file the ceiling map does not cover) on the asymmetry alone — the roster case stays green there too, which is why the both-rows leg above is the honest demonstration rather than the one-row leg.(b) the fix. Edited file, same both-rows removal:
It is the only red, and its verdict message names the file:
(c) restored.
✓ check-skill-line-ratchet self-test: 157 cases pass.at exit 0, and the live gate run at exit 0 with zero✗lines — every pin holding,pm-dispatch/SKILL.mdat 811 lines against its ceiling of 811,lanes/triage.mdat 7 against 7.Serial merge-tree proof
Both PRs were read from the API at proof time: #15955 and #16357 are open, not merged, at exactly the heads this proof targets. So the proof ran as ruled — twice, against
HEAD2c9c1a5282, the second pass with the custom merge driver disabled, because a driver's exit 0 only means the driver took the file.git merge-tree --write-tree HEAD pr15955(04b61b5f6a)ed013ccf21git merge-tree --write-tree HEAD pr16357(f5c08e9589)e9021cb245git -c merge.os-regen.driver=false merge-tree --write-tree HEAD pr15955ed013ccf21git -c merge.os-regen.driver=false merge-tree --write-tree HEAD pr16357e9021cb245The driver-disabled runs produce byte-identical trees, so no exit 0 above is a driver having absorbed the file. In both merged trees
scripts/pm/check-skill-line-ratchet.mjscarries zero conflict markers and keeps this change intact (SELF_TEST_BATTERY_FLOOR = 157present in each).Gates
Derived with
dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon the final one-file list (the tool takes its own change set from the merge baseac76425f0, three-dot). All 31 ran, each exit captured by redirecting before any pipe:✓ dispatch-gates --ran: 31 derived famil(ies) accounted for — 31 run, 0 NOT-MEASURED.check-clause2-carriers --self-testexit 0 (190 cases)check-governed-merges --test scripts/pm/check-skill-line-ratchet.mjsexit 0 —✅ NOT governed — ordinary queue landing applies to a PR with exactly this file list.check-nul-bytesexit 0 (8078 text files, no raw control bytes), plus a direct control-byte scan of the edited file: no hitspnpm lint(eslint . --no-inline-config) throughscripts/pm/os-verify-lock.sh, slotissue-15965:VERDICT command-exit 0 · held the lock 99s · waited 0sscripts/orscripts/pm/were run rather than read as silent:check-published-list-mirrors,check:pm-label-desc-cap,check:pm-governed-prose,check:single-claim-paths,check:skills-token-ratchet— all exit 0check:ratchet-remedy-authorityis worth calling out: it classifies this very file asexcluded, and itsOFFER_VERBgrammar would have reclassified it had the new verdict message spelled its remedy with an expanding verb next to a declared registry name. The message is worded to stay out of that grammar; the gate and its self-test both stay green.No changeset — repo tooling only, nothing published from any package;
skip-changesetapplied.Generated by Claude Code